#include "volumerenderer.h"
#include "../../datamanager/reg3volumedatanode.h"
#include "../../datamanager/geometrydatanode.h"
#include "../../renderer/environment.h"
#include "../../contour/mcconextractor.h"
#include "volumerendercontrol.h"

using namespace PBVP;

VolumeRenderer::VolumeRenderer()
{
	// Generate texture names
	glGenTextures(1, &m_DataTexture);
	glGenTextures(1, &m_ColormapTexture);
	m_Dim[0] = m_Dim[1] = m_Dim[2] = 0;
	m_pVolNode = NULL;
	m_pVolData = NULL;
	m_pUCData = NULL;
	m_VolRendControl = new VolumeRenderControl(this);
	m_NSlices = 256;
	m_ShowBBox = false;
	m_ShowVolume = true;
}

VolumeRenderer::~VolumeRenderer()
{
	// release resources
	glDeleteTextures(1, &m_DataTexture);
	glDeleteTextures(1, &m_ColormapTexture);
	if(m_pUCData) delete[] m_pUCData;
}

void VolumeRenderer::initialize(MainInterface *mainInterface)
{
	Plugin::initialize(mainInterface);
}

RenderPlugin* VolumeRenderer::clone()
{
	VolumeRenderer *pRenderer = new VolumeRenderer();
	pRenderer->m_pProgram = m_pProgram;
	pRenderer->m_pMainInterface = m_pMainInterface;
	return pRenderer;
}

void VolumeRenderer::begin()
{
	// TODO
	glPushAttrib(GL_ALL_ATTRIB_BITS);
	glDisable(GL_CULL_FACE);
	glDisable(GL_LIGHTING);
	glEnable(GL_BLEND);
}

void VolumeRenderer::end()
{
	// TODO
	glPopAttrib();
}

QWidget* VolumeRenderer::getRenderControl() const
{
	return m_VolRendControl;
}

bool VolumeRenderer::initRender()
{
	if (!glewIsSupported("GL_VERSION_1_3 GL_VERSION_2_0"))
	{
		return false;
	}

	GLenum err = glGetError();
	// if (err != GL_NO_ERROR) return false;

	// load the GLSL shader
	GLSLProgram *program = new GLSLProgram("shaders/volumerenderer.vert", "shaders/volumerenderer.frag");
	if(!program->isValid()) {
		fprintf(stderr, "Failed to create GLSL program for volume renderer\n");
		return false;
	}
	m_pProgram = counted_ptr<GLSLProgram> (program);

	return true;
}

bool VolumeRenderer::isSupported(DataInterface *pData) const
{
#ifdef __GNU__
	if(pData->getDataSetType() == VOLUME_DATA) return true;
#else
	Reg3DVolumeData *pVolData = dynamic_cast<Reg3DVolumeData *>(pData);
	if(pVolData) return true;
#endif
	return false;
}

void VolumeRenderer::render(DataNode *pDataNode)
{
	if(m_ShowBBox) {
		m_pVolNode->drawBoundingbox();
	}

	if(!m_ShowVolume) return;

	glDepthMask(GL_FALSE);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glActiveTexture(GL_TEXTURE0);
	glEnable(GL_TEXTURE_3D);
	glBindTexture(GL_TEXTURE_3D, m_DataTexture);
	glActiveTexture(GL_TEXTURE1);
	glEnable(GL_TEXTURE_1D);
	glBindTexture(GL_TEXTURE_1D, m_ColormapTexture);

	glUseProgram(m_pProgram->programId());
	m_pProgram->setUniform1i("dataTex", 0);
	m_pProgram->setUniform1i("cmapTex", 1);

	Vector3 cen = m_pVolNode->boundingBox().getCenter();
	Vector3 len = m_pVolNode->boundingBox().getSize();

	AxisAlignedBox bbox = m_pVolNode->boundingBox();
	AxisAlignedBox viewBox = getBoundingBoxTransformed(m_pMainInterface->getEnvironment().getViewingTransform(), bbox);
	//printf("near bound = (%f, %f, %f)\n", bbox.getMinimum().x, bbox.getMinimum().y, bbox.getMinimum().z);
	//printf("far bound = (%f, %f, %f)\n", bbox.getMaximum().x, bbox.getMaximum().y, bbox.getMaximum().z);
	//printf("transformed near bound = (%f, %f, %f)\n", viewBox.getMinimum().x, viewBox.getMinimum().y, viewBox.getMinimum().z);
	//printf("transformed far bound = (%f, %f, %f)\n", viewBox.getMaximum().x, viewBox.getMaximum().y, viewBox.getMaximum().z);
	Vector3 min = viewBox.getMinimum();
	Vector3 max = viewBox.getMaximum();

	glPushMatrix();
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glPushMatrix();
	float glmatrix[16];

	// setup automatic texture coordinate generation
	glActiveTexture(GL_TEXTURE0);
	glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
	glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

	float xplane[4] = {1/len.x, 0, 0, 0.5f-cen.x/len.x};
	float yplane[4] = {0, 1/len.y, 0, 0.5f-cen.y/len.y};
	float zplane[4] = {0, 0, 1/len.z, 0.5f-cen.z/len.z};

	m_pMainInterface->getEnvironment().getViewingTransform().toOGLArray(glmatrix);
	glMultMatrixf(glmatrix);
	glTexGenfv(GL_S, GL_EYE_PLANE, xplane);
	glTexGenfv(GL_T, GL_EYE_PLANE, yplane);
	glTexGenfv(GL_R, GL_EYE_PLANE, zplane);
	glPopMatrix();
	float offset = min.z;
	float increment = (max.z - min.z) / m_NSlices;
	glEnable(GL_BLEND);
	glBegin(GL_QUADS);
	//glMultiTexCoord3f(GL_TEXTURE0, 0.0f, 0.0f, 0.5f); glVertex3f(-len.x/2+cen.x, -len.y/2+cen.y, offset);
	//glMultiTexCoord3f(GL_TEXTURE0, 1.0f, 0.0f, 0.5f); glVertex3f(len.x/2+cen.x, -len.y/2+cen.y, offset);
	//glMultiTexCoord3f(GL_TEXTURE0, 1.0f, 1.0f, 0.5f); glVertex3f(len.x/2+cen.x, len.y/2+cen.y, offset);
	//glMultiTexCoord3f(GL_TEXTURE0, 0.0f, 1.0f, 0.5f); glVertex3f(-len.x/2+cen.x, len.y/2+cen.y, offset);
	while(offset < max.z) {
		glVertex3f(min.x, min.y, offset);
		glVertex3f(max.x, min.y, offset);
		glVertex3f(max.x, max.y, offset);
		glVertex3f(min.x, max.y, offset);
		offset += increment;
	}
	glEnd();
	glPopMatrix();
	glUseProgram(0);
}

void VolumeRenderer::setDataNode(DataNode *pDataNode)
{
#ifdef __GNU__
	m_pVolNode = (Reg3DVolumeDataNode *)(pDataNode);
#else
	m_pVolNode = dynamic_cast<Reg3DVolumeDataNode *>(pDataNode);
	assert(m_pVolNode && "Not a valid regular 3D volume data for volume renderer");
#endif
	m_pVolData = (Reg3DVolumeData *)m_pVolNode->data();

	funcMin = m_pVolData->getMinValue();
	funcMax = m_pVolData->getMaxValue();
	m_VolRendControl->setMinMaxLabels(funcMin, funcMax);

	m_Dim[0] = Math::toPowerOfTwo(m_pVolData->width());
	m_Dim[1] = Math::toPowerOfTwo(m_pVolData->height());
	m_Dim[2] = Math::toPowerOfTwo(m_pVolData->depth());

	// Maximum dimension 512x512x512
	m_Dim[0] = (m_Dim[0] > 512)? 512:m_Dim[0];
	m_Dim[1] = (m_Dim[1] > 512)? 512:m_Dim[1];
	m_Dim[2] = (m_Dim[2] > 512)? 512:m_Dim[2];

	// DEBUG: dimensions must be the same?
	// m_Dim[0] = m_Dim[1] = m_Dim[2] = 64;

	if(m_pUCData) delete[] m_pUCData;
	m_pUCData = new unsigned char[m_Dim[0]*m_Dim[1]*m_Dim[2]];
	memset(m_pUCData, 0, m_Dim[0]*m_Dim[1]*m_Dim[2]);

	m_pVolData->resampleUChar(m_pUCData, m_Dim);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_3D, m_DataTexture);
	float vfBorderColor[4] = {0.0f,0.0f,0.0f,0.0f};
	glTexParameterfv(GL_TEXTURE_3D, GL_TEXTURE_BORDER_COLOR, vfBorderColor);
	glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_R,GL_CLAMP_TO_BORDER);
	//glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_S,GL_CLAMP);
	//glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_T,GL_CLAMP);
	//glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_R,GL_CLAMP);

	glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

	glTexImage3D(GL_TEXTURE_3D, 0, GL_LUMINANCE, m_Dim[0], m_Dim[1], m_Dim[2],
		0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pUCData);

	GLenum error = glGetError();
	if (error != GL_NO_ERROR) {
		fprintf(stderr, "Error %d in loading 3D texture\n", error);
	}
	// load colormap
	loadColormapTexture(m_Colormap.toArrayOfRGBA(), ColorMap::MAP_SIZE);
	// TODO ...
}

void VolumeRenderer::reset()
{
	// TODO
}

void VolumeRenderer::loadColormapTexture(unsigned char* cmapTexture, int size)
{
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_1D, m_ColormapTexture);
	//float vfBorderColor[4] = {0.0f,0.0f,0.0f,0.0f};
	//glTexParameterfv(GL_TEXTURE_1D, GL_TEXTURE_BORDER_COLOR, vfBorderColor);
	glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_1D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER);

	glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
		cmapTexture);
}

AxisAlignedBox VolumeRenderer::getBoundingBoxTransformed(const PBVP::Matrix4 &viewMat, const PBVP::AxisAlignedBox &bbox)
{
	AxisAlignedBox boxTransformed;
	const Vector3 *corners = bbox.getAllCorners();
	Vector3 pos = viewMat * corners[0];
	Vector3 min = pos;
	Vector3 max = pos;
	for(int i = 1; i < 8; i++) {
		pos = viewMat * corners[i];
		if (min.x > pos.x) min.x = pos.x;
		if (min.y > pos.y) min.y = pos.y;
		if (min.z > pos.z) min.z = pos.z;
		if (max.x < pos.x) max.x = pos.x;
		if (max.y < pos.y) max.y = pos.y;
		if (max.z < pos.z) max.z = pos.z;
	}
	boxTransformed.setMinimum(min);
	boxTransformed.setMaximum(max);
	return boxTransformed;
}

//////////////////////////////////////////////////////////////////////////
// Public slots
//////////////////////////////////////////////////////////////////////////
void VolumeRenderer::updateView()
{
	if(m_pVolNode) {
		m_pVolNode->updateView();
	}
}

void VolumeRenderer::changeQuality(int nSlices)
{
	m_NSlices = nSlices;
	updateView();
}

void VolumeRenderer::setShowVolume(int state)
{
	if (state == Qt::Checked)
	{
		m_ShowVolume = true;
	} else {
		m_ShowVolume = false;
	}
	updateView();
}

void VolumeRenderer::setShowBBox(int state)
{
	if(state == Qt::Checked)
	{
		m_ShowBBox = true;
	} else {
		m_ShowBBox = false;
	}
	updateView();
}

GeometryDataNode* VolumeRenderer::addContour(double isoval)
{
	// rescale isoval from [0, 1] to the actual range
	double fmin = m_pVolData->getMinValue();
	double fmax = m_pVolData->getMaxValue();
	double isovalue = fmin + isoval*(fmax-fmin);

	MCContourExtractor conExtract;
	GeometryData *pSurf = conExtract.extractContour(m_pVolData, isovalue);
	GeometryDataNode *pGeoNode = new GeometryDataNode(pSurf, m_pMainInterface);
	DataManager *pDataManager = m_pMainInterface->getDataManager();
	QString name = m_pVolNode->getName().append(QString(".iso_%1").arg(isovalue));
	pGeoNode->setName(name);
	pDataManager->addDataNode(pGeoNode);
	return pGeoNode;
}

bool VolumeRenderer::updateContour(PBVP::GeometryDataNode *pGeoNode, double isoval)
{
	DataManager *pDataManager = m_pMainInterface->getDataManager();
	int id = pDataManager->findDataNode(pGeoNode);
	// not in the datamanager (may have been deleted)
	if(id < 0) return false;

	// rescale isoval from [0, 1] to the actual range
	double fmin = m_pVolData->getMinValue();
	double fmax = m_pVolData->getMaxValue();
	double isovalue = fmin + isoval*(fmax-fmin);

	MCContourExtractor conExtract;
	GeometryData *pSurf = conExtract.extractContour(m_pVolData, isovalue);
	pGeoNode->setData(pSurf);
	QString name = m_pVolNode->getName().append(QString(".iso_%1").arg(isovalue));
	pGeoNode->setName(name);
	return true;
}

bool VolumeRenderer::deleteContour(GeometryDataNode *pGeoNode)
{
	DataManager *pDataManager = m_pMainInterface->getDataManager();
	int id = pDataManager->findDataNode(pGeoNode);
	// not in the datamanager (may have been deleted)
	if(id < 0) return false;

	pDataManager->deleteDataNode(id);
	return true;
}

Q_EXPORT_PLUGIN2(volumerenderer, VolumeRenderer)
